// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace SnapObjects.Data
{
    internal class ModelQueryBuilder
        : ISqlExpressionContext
    {
        private readonly DataContext _dataContext;
        private readonly ModelSqlHelper _modelSqlHelper;
        private readonly SqlExpressionBuilder _sqlExpressionBuilder;
        private readonly ModelConditionBuilder _conditionBuilder;
        private readonly ModelTableBuilder _tableBuilder;
        private readonly IDictionary<Type, ModelQueryBuilder> _embeddedBuilders;

        public ModelQueryBuilder(DataContext dataContext, Type modelType)
        {
            _dataContext = dataContext;

            _modelSqlHelper = new ModelSqlHelper(modelType);
            _sqlExpressionBuilder = new SqlExpressionBuilder(this);

            _conditionBuilder = new ModelConditionBuilder(_modelSqlHelper, _sqlExpressionBuilder);
            _tableBuilder = new ModelTableBuilder(_modelSqlHelper, _sqlExpressionBuilder);

            _embeddedBuilders = new Dictionary<Type, ModelQueryBuilder>();
        }

        public PocoModelMeta ModelMeta => _modelSqlHelper.ModelMeta;

        public ISqlQueryBuilder SqlQueryBuilder { get; private set; }

        public IReadOnlyList<ISqlParameter> GetSqlParameters()
        {
            return ((SqlBuilderBase)this.SqlQueryBuilder).BuilderParameters;
        }

        public string BuildEmbeddedModel(Type modelType, string selectName)
        {
            if (!_embeddedBuilders.TryGetValue(modelType, out var builder))
            {
                builder = new ModelQueryBuilder(_dataContext, modelType);
                builder.BuildModelSelect(selectName);

                _embeddedBuilders.Add(modelType, builder);

                //add embedded model parameters to main SqlQueryBuilder
                this.SqlQueryBuilder.AddParameters(((SqlBuilderBase)builder.SqlQueryBuilder).BuilderParameters);
            }
            else
            {
                //Handle same model used more than one times
                builder.BuildModelSelect(selectName);
            }

            return builder.SqlQueryBuilder.ToSqlString(_dataContext);
        }

        public string BuildPropertyRef(string expression)
        {
            return _modelSqlHelper.BuildPropertyRef(expression);
        }

        public virtual string GetParameterPlacehold(string parameterId)
        {
            return this.SqlQueryBuilder.GetParameterPlacehold(parameterId);
        }

        public void BuildModelSelect(string selectName)
        {
            this.SqlQueryBuilder = SqlBuilderFactory.Create<ISqlQueryBuilder>();
            this.BuildParameters();


            if (selectName == null)
            {
                this.BuildSelect();
            }
            else
            {
                if (this.ModelMeta.SqlSelects.TryGetValue(selectName, out var select))
                {
                    select = _sqlExpressionBuilder.BuildSqlExpression(
                                    select, buildSubQueris: true, buildParameters: true);

                    this.SqlQueryBuilder.SelectRaw(select);
                }
                else
                {
                    throw new InvalidOperationException("SqlSelect not exists, selectName: " + selectName);
                }
            }

            this.BuildOtherThanSelect();
        }

        public ISqlQueryBuilder BuildFetchOne(string expression)
        {
            this.SqlQueryBuilder = SqlBuilderFactory.Create<ISqlQueryBuilder>();
            this.BuildParameters();

            this.SqlQueryBuilder.SelectRaw(expression);
            this.BuildOtherThanSelect();

            return this.SqlQueryBuilder;
        }
        public string GetFetchOneExpression(string expression)
        {
            this.SqlQueryBuilder = SqlBuilderFactory.Create<ISqlQueryBuilder>();
            this.BuildParameters();

            return _sqlExpressionBuilder.BuildSqlExpression(
                                expression, buildSubQueris: true, buildParameters: true);
        }

        private void BuildParameters()
        {
            var guid = Guid.NewGuid();

            foreach (var parameter in _modelSqlHelper.ModelMeta.SqlParameters)
            {
                ((SqlBuilderBase)this.SqlQueryBuilder).AddParameters(
                    SqlBuilder.Parameter(parameter.DataType, parameter.Name, guid.ToString()));
            }
        }

        private void BuildOtherThanSelect()
        {
            this.BuildFromAndJoin();

            this.BuildWhere();

            this.BuildGroupBy();

            this.BuildHaving();

            this.BuildOrderBy();

            this.BuildUnion();
        }

        private void BuildUnion()
        {
            foreach (var union in this.ModelMeta.SqlUnions)
            {
                var sql = union.RawSql;

                if (union.IsRawSql)
                {
                    sql = _sqlExpressionBuilder.BuildSqlExpression(sql,
                                                buildSubQueris: true, buildParameters: true);
                }
                else
                {
                    sql = this.BuildEmbeddedModel(union.ModelType, union.SelectName);
                }

                if (union.UnionAll)
                {
                    this.SqlQueryBuilder.UnionAll(sql);
                }
                else
                {
                    this.SqlQueryBuilder.Union(sql);
                }
            }
        }

        public ISqlQueryBuilder BuildFetchOneByKey(string expression)
        {
            this.SqlQueryBuilder = SqlBuilderFactory.Create<ISqlQueryBuilder>();

            var properties = this.ModelMeta.UpdatableProperties;

            this.SqlQueryBuilder.SelectRaw(expression);

            var fromTable = this.ModelMeta.GetFullTableName(PocoModelMeta.TABLE_ID);

            this.SqlQueryBuilder.From(fromTable);

            _modelSqlHelper.BuildWhereForKeys(this.SqlQueryBuilder, properties);

            return this.SqlQueryBuilder;
        }

        public ISqlQueryBuilder BuildSelectIgnoreWhere()
        {
            this.SqlQueryBuilder = SqlBuilderFactory.Create<ISqlQueryBuilder>();

            var properties = this.ModelMeta.UpdatableProperties;

            foreach (var property in properties)
            {
                this.SelectColumn(property);
            }

            var fromTable = this.ModelMeta.GetFullTableName(PocoModelMeta.TABLE_ID);

            if (!String.IsNullOrWhiteSpace(this.ModelMeta.MappedTableAlias))
            {
                this.SqlQueryBuilder.From(fromTable, this.ModelMeta.MappedTableAlias);
            }
            else
            {
                this.SqlQueryBuilder.From(fromTable);
            }

            return this.SqlQueryBuilder;
        }

        public ISqlQueryBuilder BuildSelectByKey()
        {
            _modelSqlHelper.BuildWhereForKeys(this.BuildSelectIgnoreWhere(), this.ModelMeta.UpdatableProperties);

            return this.SqlQueryBuilder;
        }

        private void BuildSelect()
        {
            var properties = this.ModelMeta.PropertyMap
                                        .Where(o => o.Value.IsBindable)
                                        .OrderBy(o => o.Value.Index)
                                        .Select(p => p.Value).ToList();

            if (this.ModelMeta.Distinct)
            {
                this.SqlQueryBuilder.Distinct();
            }

            if (this.ModelMeta.TopDefinded)
            {
                this.SqlQueryBuilder.Top(this.ModelMeta.TopRows);
            }

            //Build select statement
            foreach (var property in properties)
            {
                this.SelectColumn(property);
            }
        }

        private void BuildFromAndJoin()
        {
            _tableBuilder.BuildFromAndJoin(this.SqlQueryBuilder, this.ModelMeta.Tables);
        }

        private void BuildWhere()
        {
            _conditionBuilder.BuildWhere(this.SqlQueryBuilder);
        }

        private void BuildHaving()
        {
            _conditionBuilder.BuildHaving(this.SqlQueryBuilder);
        }

        private void BuildGroupBy()
        {
            if (this.ModelMeta.SqlGroupBy == null)
            {
                return;
            }

            var expressions = new List<string>();

            foreach (var expression in this.ModelMeta.SqlGroupBy.GroupByExpressions)
            {
                expressions.Add(_sqlExpressionBuilder.BuildSqlExpression(expression));
            }

            this.SqlQueryBuilder.GroupBy(expressions.ToArray());
        }

        private void BuildOrderBy()
        {
            if (this.ModelMeta.SqlOrderBy == null)
            {
                return;
            }

            var expressions = new List<string>();

            foreach (var expression in this.ModelMeta.SqlOrderBy.OrderByExpressions)
            {
                expressions.Add(_sqlExpressionBuilder.BuildSqlExpression(expression));
            }

            this.SqlQueryBuilder.OrderByRaw(new StringBuilder()
                            .AppendValues(expressions, separator: ",").ToString());
        }

        private void SelectColumn(IPocoProperty property)
        {
            var column = this.BuildColumn(property);

            if (column.Item3) //Compute
            {
                if (column.Item2 != null) //column alias
                {
                    this.SqlQueryBuilder.SelectRaw(column.Item1 + " AS " + column.Item2);
                }
                else
                {
                    this.SqlQueryBuilder.SelectRaw(column.Item1);
                }
            }
            else
            {
                if (column.Item2 != null) //column alias
                {
                    this.SqlQueryBuilder.Select(column.Item1, column.Item2);
                }
                else
                {
                    this.SqlQueryBuilder.Select(column.Item1);
                }
            }
        }

        private (string, string, bool) BuildColumn(IPocoProperty property)
        {
            var column = property.MappedColumn;
            string alias = null;
            var isCompute = false;

            //foreign properties
            if (property.IsForeign || property.SqlCompute != null)
            {
                if (property.IsForeign)
                {
                    column = property.SqlColumn.TableAlias + "." + property.SqlColumn.Column;

                    if (property.SqlColumn.ColumnAliasDefined)
                    {
                        alias = property.SqlColumn.ColumnAlias;
                    }
                }
                else if (property.SqlCompute != null)
                {
                    isCompute = true;
                    column = property.SqlCompute.Expression;
                    column = _sqlExpressionBuilder.BuildSqlExpression(
                                    column, buildSubQueris: true, buildParameters: true);

                    if (property.SqlCompute.AliasDefined)
                    {
                        alias = property.SqlCompute.Alias;
                    }
                    else
                    {
                        alias = property.Name;
                    }
                }
            }
            else if (this.ModelMeta.HasJoinTable) //non - foreign/computer properties
            {
                column = _modelSqlHelper.GetMappedTableAliasOrName() + "." + column;

                if (property.SqlColumn != null)
                {
                    alias = property.SqlColumn.ColumnAliasDefined ? property.SqlColumn.ColumnAlias : alias;
                }
                else if (property.Attribute.ColumnDefined)
                {
                    alias = property.Attribute.Column.Name;
                }
                else if (!property.MappedColumn.Equals(property.Name, StringComparison.OrdinalIgnoreCase))
                {
                    alias = property.Name;
                }
            }
            else if (property.SqlColumn != null && property.SqlColumn.TableAliasDefined)
            {
                column = property.SqlColumn.TableAlias + "." + property.SqlColumn.Column;
            }

            return (column, alias, isCompute);
        }
    }
}
